package com.linkdood.app.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.linkdood.app.constants.ErrorCode;
import com.linkdood.app.domain.AccountUploadFile;
import com.linkdood.app.domain.UploadFile;
import com.linkdood.app.dto.UploadFileDTO;
import com.linkdood.app.exception.GeneralException;
import com.linkdood.app.exception.InvalidParamException;
import com.linkdood.app.mapper.UploadFileMapper;
import com.linkdood.app.service.AccountUploadFileService;
import com.linkdood.app.service.UploadFileService;
import com.linkdood.app.service.ZipService;
import com.linkdood.app.utils.DateTimeUtil;
import com.linkdood.app.utils.FileDigestUtil;
import com.linkdood.app.utils.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

/**
 * 文件上传下载操作功能
 */
@Service
@Slf4j
public class UploadFileServiceImpl extends ServiceImpl<UploadFileMapper, UploadFile> implements UploadFileService {

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Value("${customize.upload.path}")
    private String uploadPath;

    @Value("${customize.upload.file-expire-time}")
    private Long commonExpireTime;

    @Value("${customize.upload.temp-file-expire-time}")
    private Long tempExpireTime;

    @Value("${customize.upload.sign-expired-file}")
    private Boolean signFile;

    @Value("${customize.upload.sign-expired-temp-file}")
    private Boolean signTempFile;

    @Value("${vrv.web.serviceHost}")
    private String serviceHost;

    public static final String IOS_GALLERY_IMAGE_SUFFIX = "HEIC";

    private final AccountUploadFileService accountUploadFileService;
    private final ZipService zipService;

    public UploadFileServiceImpl(AccountUploadFileService accountUploadFileService, ZipService zipService) {
        this.accountUploadFileService = accountUploadFileService;
        this.zipService = zipService;
    }


    @Override
    public List<UploadFileDTO> listFilesByAccountId(Integer page, Integer size, String accountId) {
        if (null == page || page < 1) {
            page = 1;
        }
        if (size == null || size <= 0) {
            size = 50;
        }
        long pageSkip = (long) (page - 1) * size;
        Integer temp = 0;
        Integer delete = 0;
        List<UploadFileDTO> uploadFiles = getBaseMapper().selectAccountFileByParam("", temp, delete, pageSkip,
                size, "create_time", "desc", accountId);
        for (UploadFileDTO uploadFile : uploadFiles) {
            uploadFile.setStaticFilePath(contextPath + "/api/static-files/" +
                    uploadFile.getPath().replaceFirst("./", ""));
        }
        return uploadFiles;
    }

    /**
     * 文件上传
     */
    @Override
    public void saveFile(MultipartFile multipartFile, String currentAccountId) {
        if (null == multipartFile || StringUtils.isEmpty(multipartFile.getOriginalFilename())) {
            throw new InvalidParamException("multipartFile");
        }
        UploadFile uploadFile = getUploadDir(multipartFile);
        if (null == uploadFile) {
            return;
        }
        File absoluteFile = new File(uploadFile.getPath());
        File localFile = new File(absoluteFile.getAbsolutePath());
        if (!localFile.getParentFile().exists()) {
            localFile.getParentFile().mkdirs();
        }
        try {
            multipartFile.transferTo(localFile);
        } catch (Exception e) {
            log.error("File transfer error:{}", e.getMessage());
            throw new GeneralException(ErrorCode.ERROR_FILE_TRANSFER);
        }
        handleFileNameAndFileContent(uploadFile, currentAccountId, localFile);
    }

    @Override
    public Page<UploadFileDTO> pageUploadFiles(String name, Integer page, Integer size, String sort, String currentAccountId) {
        Page<UploadFileDTO> pages = new Page<>();
        // 设置分页数量，页数不小于 1
        if (null == page || page < 1) {
            page = 1;
        }
        // 页面容量 最大200，最小为1
        if (size == null || size <= 0) {
            size = 1;
        } else if (size > 200) {
            size = 200;
        }
        String sortFiled = null;
        String sortType = null;
        if (!StringUtils.isEmpty(sort)) {
            String[] split = sort.split(",");
            if (null != split && split.length > 1) {
                sortFiled = split[0];
                sortType = split[1];
            }
        }
        long pageSkip = (long) (page - 1) * size;
        int temp = 0;
        int delete = 0;
        // 分页连接查询
        List<UploadFileDTO> uploadFiles = getBaseMapper().selectAccountFileByParam(name, temp, delete, pageSkip,
                size, sortFiled, sortType, currentAccountId);
        for (UploadFileDTO uploadFileDTO : uploadFiles) {
            if (signFile) {
                uploadFileDTO.setExpireTime(uploadFileDTO.getCreateTime().plusMinutes(commonExpireTime));
            }
            uploadFileDTO.setPath(serviceHost + contextPath + "/api/files/download-url/" + uploadFileDTO.getId());
        }
        long count = getBaseMapper().selectCountByParam(name, temp, delete, currentAccountId);
        pages.setSize(size);
        pages.setTotal(count);
        pages.setRecords(uploadFiles);
        return pages;
    }

    /**
     * 获取上传文件地址
     */
    private UploadFile getUploadDir(MultipartFile multipartFile) {
        String fileName = multipartFile.getOriginalFilename();
        if (StringUtils.isEmpty(fileName)) {
            return null;
        }
        // 防止文件过多，按日期生成文件夹
        String fileDir = getDirFilePath();
        File file = new File(fileDir);
        if (!file.exists() && !file.mkdirs()) {
            throw new GeneralException(ErrorCode.ERROR_FOLDER_CREATE);
        }
        UploadFile uploadFile = new UploadFile();
        uploadFile.setName(fileName);
        uploadFile.setPath(fileDir + "/" + getUUIDFileName(fileName));
        uploadFile.setSize(multipartFile.getSize());
        return uploadFile;
    }

    @Override
    public String getUUIDFileName(String fileName) {
        return UUID.randomUUID() + fileName.substring(fileName.lastIndexOf("."));
    }

    /**
     * 多文件下载zip地址
     */
    @Override
    public String getDownloadFileUrl(String id, String currentAccountId) {
        if (StringUtils.isEmpty(id)) {
            return null;
        }
        UploadFile uploadFile;
        // 判断是否是多文件下载
        if (isMultipleFiles(id)) {
            uploadFile = getZipFile(id);
            if (null == uploadFile) {
                return null;
            }
            save(uploadFile);
            accountUploadFileService.saveFileAccount(uploadFile.getId(), currentAccountId);
        } else {
            uploadFile = getById(id);
        }
        return serviceHost + contextPath + "/api/files/download-url/" + uploadFile.getId();
    }

    @Override
    public UploadFile getZipFile(String ids) {
        if (StringUtils.isEmpty(ids)) {
            return null;
        }
        String[] split = ids.split(",");
        if (null == split || split.length < 1) {
            return null;
        }
        List<File> localFiles = new ArrayList<>();
        List<String> realFilenames = new ArrayList<>();
        for (String id : split) {
            UploadFile uploadFile = getBaseMapper().selectById(id);
            if (null == uploadFile) {
                log.warn("文件不存在：{}", id);
                continue;
            }
            if (StringUtils.isEmpty(uploadFile.getPath()) || Boolean.TRUE.equals(uploadFile.getDeleted())) {
                log.warn("文件路径不存在或文件已经过期：{}", id);
                continue;
            }
            File file = new File(uploadFile.getPath());
            localFiles.add(file);
            realFilenames.add(uploadFile.getName());
        }
        if (localFiles.isEmpty()) {
            return null;
        }
        // 临时文件路径
        String tempZipFilePath = getDirFilePath() + "/" + "temp_" + getUUIDFileName(".zip");
        // 设置临时文件
        File zipFile = new File(tempZipFilePath);
        File parentFile = zipFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        // 判断是否可读、可写
        if (!parentFile.canWrite() || !parentFile.canRead()) {
            log.error("Dir can not read or write：{}", parentFile.getPath());
            throw new GeneralException(ErrorCode.ERROR_FOLDER_READ_OR_WRITE);
        }
        zipService.compressFiles2Zip(localFiles, zipFile, realFilenames);
        UploadFile uploadFile = new UploadFile();
        uploadFile.setName(zipFile.getName());
        uploadFile.setTemp(true);
        // 处理临时文件路径
        uploadFile.setPath(tempZipFilePath);
        return uploadFile;
    }

    /**
     * 分片上传第一次请求
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String uploadFragment(long fileSize) {
        UploadFile uploadFile = new UploadFile();
        // 只需要记录文件大小
        uploadFile.setSize(fileSize);
        save(uploadFile);
        return uploadFile.getId();
    }

    /**
     * 分片上传逻辑
     */
    @Override
    public void saveFileAsPatch(String uploadFileId, HttpServletRequest request, String accountId) {
        // 分片上传
        if (StringUtils.isEmpty(uploadFileId)) {
            throw new InvalidParamException("uploadFileId");
        }
        UploadFile uploadFile = lambdaQuery().eq(UploadFile::getId, uploadFileId).one();
        if (null == uploadFile) {
            throw new GeneralException(ErrorCode.ERROR_FILE_NOT_EXIST);
        }
        if (StringUtils.isEmpty(uploadFile.getPath())) {
            String fileName = getFileName(request.getHeader("Upload-Name"));
            uploadFile.setPath(getDirFilePath() + "/" + getUUIDFileName(fileName));
            updateById(uploadFile);
        }
        long uploadOffset = Long.parseLong(request.getHeader("Upload-Offset"));
        File localFile = new File(uploadFile.getPath());
        ServletInputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            writeFile(localFile, inputStream, uploadOffset);
            // 等文件上传结束后，再检验文件是否重复
            if (uploadFile.getSize().equals(localFile.length())) {
                String fileName = getFileName(request.getHeader("Upload-Name"));
                uploadFile.setName(fileName);
                handleFileNameAndFileContent(uploadFile, accountId, localFile);
            }
        } catch (Exception e) {
            log.error("File write error:{}", e.getMessage());
        } finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
            } catch (Exception e) {
                log.error("ServletInputStream close error:{}", e.getMessage());
            }
        }
    }

    /**
     * 写入文件
     */
    public void writeFile(File localFile, InputStream inputStream, long uploadOffset) {
        try (RandomAccessFile accessFile = new RandomAccessFile(localFile, "rw");
             ByteArrayOutputStream baos = new ByteArrayOutputStream()
        ) {
            int len = 1024;
            byte[] bytes = new byte[len];
            int i;
            while ((i = inputStream.read(bytes, 0, len)) > 0) {
                baos.write(bytes, 0, i);
            }
            accessFile.seek(uploadOffset);
            accessFile.write(baos.toByteArray());
        } catch (Exception e) {
            log.error("Sharded storage error:{}", e.getMessage());
        }
    }

    /**
     * 获取文件名
     */
    private String getFileName(String uploadName) {
        byte[] bytes = uploadName.getBytes(StandardCharsets.ISO_8859_1);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    /**
     * 检查是否重复文件名
     */
    private boolean isDuplicateFilename(String fileName, String currentAccountId) {
        long count = getBaseMapper().
                selectCurrentUserUnDeletedUploadFile(0, 0, currentAccountId, null, fileName);
        return count > 0;
    }

    /**
     * 查询文件 MD5 值是否和表中重复
     */
    private boolean isDuplicateContent(String md5, String currentAccountId) {
        long count = getBaseMapper().
                selectCurrentUserUnDeletedUploadFile(0, 0, currentAccountId, md5, null);
        return count > 0;
    }

    /**
     * 获取文件路径
     */
    private String getDirFilePath() {
        String dateDir = uploadPath + DateTimeUtil.nowFormatToYMD();
        File file = new File(dateDir);
        if (!file.exists()) {
            file.mkdirs();
        }
        // 判断是否可读、可写
        if (!file.canWrite() || !file.canRead()) {
            log.error("Dir can not read or write：{}", file.getPath());
            throw new GeneralException(ErrorCode.ERROR_FOLDER_READ_OR_WRITE);
        }
        return dateDir;
    }

    /**
     * 检查文件名或文件内容是否已存在
     */
    private synchronized void handleFileNameAndFileContent(UploadFile uploadFile, String accountId, File localFile) {
        boolean duplicateFileName = isDuplicateFilename(uploadFile.getName(), accountId);
        if (duplicateFileName) {
            removeLocalFileAndRecord(uploadFile);
            throw new GeneralException(ErrorCode.ERROR_FILENAME_DUPLICATE);
        }

        String fileMd5 = FileDigestUtil.getFileMd5(localFile);
        if (StringUtils.isEmpty(fileMd5)) {
            throw new GeneralException(ErrorCode.ERROR_FILE_GENERATE_MD5);
        }
        uploadFile.setMd5(fileMd5);
        boolean duplicateFileContent = isDuplicateContent(uploadFile.getMd5(), accountId);
        if (duplicateFileContent) {
            removeLocalFileAndRecord(uploadFile);
            throw new GeneralException(ErrorCode.ERROR_FILE_CONTENT_DUPLICATE);
        }

        // 如果文件名或文件内容没有重复,更新或插入文件表，插入关联表
        saveOrUpdate(uploadFile);
        accountUploadFileService.saveFileAccount(uploadFile.getId(), accountId);
    }

    /**
     * 删除本地文件和文件记录
     */
    private void removeLocalFileAndRecord(UploadFile uploadFile) {
        File localFile = new File(uploadFile.getPath());
        localFile.delete();
        getBaseMapper().deleteById(uploadFile.getId());
    }

    /**
     * 删除多个文件
     */
    @Override
    public void deleteLocalFiles(String ids) {
        if (StringUtils.isEmpty(ids)) {
            return;
        }
        String[] split = ids.split(",");
        if (null != split && split.length > 0) {
            for (String id : split) {
                // 首先查询文件数据库
                UploadFile uploadFile = lambdaQuery().eq(UploadFile::getId, id).one();
                // 存在则删除
                if (null == uploadFile) {
                    continue;
                }
                // 删除db中表的数据
                getBaseMapper().deleteById(id);
                // 删除本地文件
                File localFile = new File(uploadFile.getPath());
                localFile.delete();
                deleteEmptyDir(localFile.getParentFile());
                // 删除关联表
                accountUploadFileService.removeByFileId(id);
            }
        }
    }

    /**
     * 定时任务删除无效文件
     */
    @Override
    public void deleteInvalidFile() {
        List<UploadFile> removeFiles = lambdaQuery().eq(UploadFile::getDeleted, true).list();
        if (CollectionUtils.isEmpty(removeFiles)) {
            return;
        }
        for (UploadFile uploadFile : removeFiles) {
            File file = new File(uploadFile.getPath());
            file.delete();
            getBaseMapper().deleteById(uploadFile.getId());
            // 删除中间表的信息
            accountUploadFileService.removeByFileId(uploadFile.getId());
            // 是否需要删除文件夹
            deleteEmptyDir(file.getParentFile());
        }
    }

    private void deleteEmptyDir(File file) {
        if (null == file || !file.isDirectory()) {
            return;
        }
        String[] list = file.list();
        if (list != null
                && list.length < 1
                && !file.getPath().contains(DateTimeUtil.nowFormatToYMD())) {
            // 文件夹需要删除
            file.delete();
        }
    }

    /**
     * 定时任务把超过一定时间的文件置为无效
     */
    @Override
    public void setInvalidFile() {
        // 是否开启临时文件过期标记
        if (signTempFile) {
            setInvalidFiles(true, tempExpireTime);
        }

        // 是否开启用户上传文件过期标记
        if (signFile) {
            setInvalidFiles(false, commonExpireTime);
        }
    }

    /**
     * 设置过期时间
     *
     * @param isTemp 是否是临时文件
     * @param time   设置的过期时间
     */
    private void setInvalidFiles(boolean isTemp, long time) {
        List<UploadFile> uploadFiles = lambdaQuery()
                .eq(UploadFile::getTemp, isTemp)
                .eq(UploadFile::getDeleted, false)
                .list();
        uploadFiles.forEach(uploadFile -> {
            if (null == uploadFile.getCreateTime()) {
                return;
            }
            if (uploadFile.getCreateTime().plusMinutes(time)
                    .isBefore(LocalDateTime.now())) {
                uploadFile.setDeleted(true);
                updateById(uploadFile);
            }
        });
    }

    /**
     * 检查是多个文件ID
     */
    private boolean isMultipleFiles(String id) {
        if (StringUtils.isEmpty(id)) {
            throw new GeneralException(ErrorCode.ERROR_FILE_NOT_EXIST);
        }
        return id.contains(",");
    }

    @Override
    public void getDownloadFile(String id, HttpServletResponse response) {
        UploadFile uploadFile = getBaseMapper().selectById(id);
        File file = new File(uploadFile.getPath());
        response.setContentType("application/force-download");
        if (file.exists()) {
            // 设置强制下载不打开
            // 设置文件名
            byte[] buffer = new byte[1024];
            try (FileInputStream fis = new FileInputStream(file);
                 BufferedInputStream bis = new BufferedInputStream(fis)) {
                response.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(uploadFile.getName(), StandardCharsets.UTF_8.name()));
                OutputStream os = response.getOutputStream();
                int i = bis.read(buffer);
                while (i != -1) {
                    os.write(buffer, 0, i);
                    i = bis.read(buffer);
                }
            } catch (Exception e) {
                log.error("下载出错：{}", e.getMessage());
            }
        }
    }

    /**
     * 下载 fileUrl 资源中的图片，并生成缩略图
     */
    @Async
    @Override
    public String uploadImages(String fileUrl, String filenamePrefix, String end) {
        File fileDir = new File(uploadPath);
        if (!fileDir.isDirectory()) {
            throw new GeneralException(ErrorCode.ERROR_CONFIGURATION_FILE);
        }

        String filename = filenamePrefix + "." + end;
        String thumbnailFileUrl = uploadPath + filenamePrefix + "_thumbnail." + end;

        Path path = Paths.get(uploadPath + filename);
        if (path.toFile().exists()) {
            return filename;
        }

        RestTemplate restTemplate;
        try {
            restTemplate = HttpUtil.restTemplate(fileUrl);
            RequestCallback requestCallback = request -> request.getHeaders()
                .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
            File file = restTemplate.execute(fileUrl, HttpMethod.GET, requestCallback, clientHttpResponse -> {
                Files.copy(clientHttpResponse.getBody(), path);
                return path.toFile();
            });
            if (file != null && file.isFile()) {
                // 如果是 ISO11手机相册图片，格式为.HEIC，该图片无法进行缩略图转换，则不生成缩略图
                // 详细问题描述： https://github.com/coobird/thumbnailator/issues/160
                if (IOS_GALLERY_IMAGE_SUFFIX.equals(end)) {
                    return filename;
                }
                // 根据民情通项目经验，当原图太大时前端加载较慢，所以需要生成缩略图，方便渲染显示, 此处是将图片尺寸缩小一半，质量变为原来的 1/4
                Thumbnails.of(uploadPath + file.getName()).scale(0.5f).outputQuality(0.25f).toFile(thumbnailFileUrl);
                // 将下载的原图和生成的缩略图加入到系统的文件处理流程中
                List<UploadFile> uploadFiles = new ArrayList<>();
                UploadFile originalImage = new UploadFile();
                originalImage.setName(path.getFileName().toString());
                originalImage.setPath(path.toString());
                uploadFiles.add(originalImage);

                File thumbnailFile = new File(thumbnailFileUrl);
                UploadFile thumbnailImage = new UploadFile();
                thumbnailImage.setName(thumbnailFile.getName());
                thumbnailImage.setPath(thumbnailFile.toString());
                uploadFiles.add(thumbnailImage);
                saveBatch(uploadFiles);
                return filename;
            } else {
                throw new GeneralException(ErrorCode.ERROR_FILE_TRANSFER);
            }
        } catch (Exception e) {
            log.error("{}", e);
            path.toFile().deleteOnExit();
            throw new GeneralException(ErrorCode.ERROR_FILE_TRANSFER);
        }
    }

    @Override
    public List<UploadFile> listFileByPath(String path) {
        return lambdaQuery().eq(UploadFile::getPath, path).list();
    }
}

